// Copyright (c) 2025, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// This software is dual-licensed to you under the Universal Permissive License
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
// either license.
//
// If you elect to accept the software under the Apache License, Version 2.0,
// the following applies:
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//-----------------------------------------------------------------------------
'use strict';
const oracledb = require('oracledb');
const identitydataplane = require("oci-identitydataplane");
const common = require("oci-common");
const { generateKeyPair } = require('crypto');
const fs = require('fs');

async function getToken(params) {
  switch (params.authType.toLowerCase()) {
    case 'configfilebasedauthentication':
      return await configFileBasedAuthentication(params);
    case 'simpleauthentication':
      return await simpleAuthentication(params);
    case 'instanceprincipal':
      return await instancePrincipalAuthentication(params);
    default:
      throwErr(`Invalid authentication type ${params.authType} in extensionOci plugins.`);
  }
}

//---------------------------------------------------------------------------
// throwErr()
//---------------------------------------------------------------------------
function throwErr(message) {
  throw new Error(message);
}

//---------------------------------------------------------------------------
// Requests an access token from the dataplane service
// Generating security token
//---------------------------------------------------------------------------
async function generateAccessToken(provider, scope) {
  // Scope uses the * character to identify all databases in the cloud
  // tenancy of the authenticated user. urn:oracle:db::id::*, default

  // A scope that authorizes access to all databases within a compartment has
  // the form: urn:oracle:db::id::<compartment-ocid>
  // String scope = "urn:oracle:db::id::ocid1.compartment.oc1..xxxxxxxx"

  // A scope that authorizes access to a single database within a compartment
  // has the form: urn:oracle:db::id::<compartment-ocid>::<database-ocid>
  // String scope = "urn:oracle:db::id::ocid1.compartment.oc1..xxxxxx::ocid1.autonomousdatabase.oc1.phx.xxxxxx"

  const client = new identitydataplane.DataplaneClient({
    authenticationDetailsProvider: provider
  });
  const keyPair = await _getKeyPair();

  const generateScopedAccessTokenRequest = {
    generateScopedAccessTokenDetails: {
      scope: scope ?? "urn:oracle:db::id::*",
      publicKey: keyPair.publicKey
    }
  };

  const generateScopedAccessTokenResponse =
    await client.generateScopedAccessToken(generateScopedAccessTokenRequest);

  return {
    token: generateScopedAccessTokenResponse.securityToken.token,
    privateKey: keyPair.privateKey
  };
}

//---------------------------------------------------------------------------
// Generates a public-private key pair for proof of possession when token
// requested by this provider is presented for validation.
//---------------------------------------------------------------------------
async function _getKeyPair() {
  return await new Promise((resolve, reject) => {
    generateKeyPair('rsa', {
      modulusLength: 4096,
      publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
      },
      privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem',
      }
    }, (err, publicKey, privateKey) => {
      if (err) return reject(err);
      resolve({publicKey, privateKey});
    });
  });
}

//---------------------------------------------------------------------------
//  User defined function for reading token and private key values
//  generated by the OCI SDK.
//  Returns the OCI SDK's token request details object for the given
//  config parameters, where a scope parameter specifies the
//  scope of requested access. The request will specify a public key
//  as being paired with a private key that the presenter of the token must
//  prove to be in possession of.
//
//  The path and profile of the config file may be configured by optional
//  parameters in object accessTokenConfig. If values not provided in
//  accessTokenConfig then this method will read the DEFAULT
//  profile from $HOME/.oci/config.
//  accessTokenConfig.configFileLocation, Not null.
//  accessTokenConfig.profile, Not null.
//
//  Return API Key-Based Authentication.
//---------------------------------------------------------------------------
async function configFileBasedAuthentication(accessTokenConfig) {
  const provider =
    new common.ConfigFileAuthenticationDetailsProvider(accessTokenConfig.configFileLocation, accessTokenConfig.profile);

  return await generateAccessToken(provider, accessTokenConfig.scope);
}

//---------------------------------------------------------------------------
// simpleAuthentication()
// Returns authentication details for the provided credentials.
//   tenancy: OCID of the tenancy
//   user: OCID of the user
//   fingerprint: Fingerprint of the public key
//   privateKey: Private key
//   passPhrase: Passphrase that is used to encrypt the private key.
//               null if not used.
// Return API Key-Based Authentication.
//---------------------------------------------------------------------------
async function simpleAuthentication(accessTokenConfig) {
  const tenancy = accessTokenConfig.tenancy ??
    throwErr("Token based authentication config parameter tenancy is missing for simpleauthentication in extensionOci plugins.");
  const user = accessTokenConfig.user ??
    throwErr("Token based authentication config parameter user is missing for simpleauthentication in extensionOci plugins.");
  const fingerprint = accessTokenConfig.fingerprint ??
    throwErr("Token based authentication config parameter fingerprint is missing for simpleauthentication in extensionOci plugins.");
  const passphrase = accessTokenConfig.passphrase |= null; // optional
  const privateKeyLocation = accessTokenConfig.privateKeyLocation ??
    throwErr("Token based authentication config parameter privateKeyLocation is missing for simpleauthentication in extensionOci plugins.");
  const privateKey = fs.readFileSync(privateKeyLocation, 'utf-8'); // ~/.oci/oci_api_key.pem
  const regionId = accessTokenConfig.regionId ??      // ex : us-ashburn-1
    throwErr("Token based authentication config parameter regionId is missing for simpleauthentication in extensionOci plugins.");

  let region;
  const regionsList = common.Region.values();
  regionsList.forEach(regions => {
    if (regions.regionId === regionId) {
      region = regions;
      return;
    }
  });
  const provider = new common.SimpleAuthenticationDetailsProvider(
    tenancy,
    user,
    fingerprint,
    privateKey,
    passphrase,
    region
  );

  return await generateAccessToken(provider, accessTokenConfig.scope);
}

//---------------------------------------------------------------------------
// instancePrincipalAuthentication()
//
// Authentication in a Compute Instance, without credentials, as an instance
// principal. This authentication method should only work on compute
// instances where internal network endpoints are reachable.
// Return Instance Principal Authentication.
//---------------------------------------------------------------------------
async function instancePrincipalAuthentication(accessTokenConfig) {
  const provider = await new common.InstancePrincipalsAuthenticationDetailsProviderBuilder().build();

  return await generateAccessToken(provider, accessTokenConfig.scope);
}

//---------------------------------------------------------------------------
//  hookFn()
//  hookFn will get registerd to driver while loading plugins.
//---------------------------------------------------------------------------
function hookFn(options) {
  if (options.tokenAuthConfigOci) {
    // eslint-disable-next-line no-unused-vars
    options.accessToken = async function callbackFn(refresh, config) {
      return await getToken(config);
    };
    options.accessTokenConfig = options.tokenAuthConfigOci;
  }
}
oracledb.registerProcessConfigurationHook(hookFn);
